﻿using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Threading;
using System.Linq;
using Citrix.Management.Automation;
using Citrix.XaAdminMobile.Util;
using Citrix.XenApp.Commands;
using Citrix.XenApp.Sdk;
using Citrix.Cmp;
using System.ComponentModel;

namespace Citrix.XaAdminMobile.Model
{
    /// <summary>
    /// The servers model object manages the query and caching of XenApp server state.
    /// </summary>
    internal class ServersModel : IServersModel
    {
        /// <summary>
        /// The known set of servers, keyed by their name.
        /// </summary>
        private Dictionary<string, ServerModel> servers =
            new Dictionary<string, ServerModel>(StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// Used to sync access to internal server state.
        /// </summary>
        private object stateLock = new object();

        /// <summary>
        /// Ref back to the base/parent model object.
        /// </summary>
        private readonly DefaultModel modelBase;

        /// <summary>
        /// Reference count of whether or not we are loading state.
        /// </summary>
        private long loadingCount = 0;

        /// <summary>
        /// Fired whenever a new server is detected or an existing server is removed from the
        /// farm.
        /// </summary>
        public event EventHandler<ServersChangedArgs> ServersChanged;

        /// <summary>
        /// The last error returned when loading server state - null means no error.
        /// </summary>
        private string lastLoadError = null;

        /// <summary>
        /// Intialisation ctor.
        /// </summary>
        /// <param name="modelRoot">A ref to the root model object.</param>
        /// <exception cref="ArgumentNullException">Thrown if modelRoot is null.</exception>
        internal ServersModel(DefaultModel modelRoot)
        {
            if (null == modelRoot)
            {
                throw new ArgumentNullException("modelRoot");
            }

            this.modelBase = modelRoot;
            Refresh();
        }

        /// <summary>
        /// Whether or not the model is currently loading new server state.
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return (0 < Interlocked.Read(ref this.loadingCount));
            }
        }

        /// <summary>
        /// Returns the last error (if any) when loading server state. Null means the last load
        /// was successful.
        /// </summary>
        public string LastLoadError
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.lastLoadError;
                }
            }
            private set
            {
                bool changed = false;
                lock (this.stateLock)
                {
                    if (!string.Equals(value, this.lastLoadError))
                    {
                        changed = true;
                        this.lastLoadError = value;
                    }
                }

                if (changed)
                {
                    OnPropertyChanged("LastLoadError");
                }
            }
        }

        /// <summary>
        /// Refreshes all the server state.
        /// </summary>
        public void Refresh()
        {
            RefreshCoreServerState();
            RefreshServerLoad();
        }

        /// <summary>
        /// The list of currently known servers.
        /// </summary>
        public List<IServerModel> Servers
        {
            get
            {
                lock (this.stateLock)
                {
                    return new List<IServerModel>(this.servers.Values);
                }
            }
        }

        /// <summary>
        /// Increments the working/loading count.
        /// </summary>
        private void IncrementLoading()
        {
            if (1 == Interlocked.Increment(ref this.loadingCount))
            {
                OnPropertyChanged("IsLoading");
            }
        }

        /// <summary>
        /// Decrements the working/loading count.
        /// </summary>
        private void DecrementLoading()
        {
            if (0 == Interlocked.Decrement(ref this.loadingCount))
            {
                OnPropertyChanged("IsLoading");
            }
        }

        /// <summary>
        /// Does a server query to reload all the core server state.
        /// </summary>
        private void RefreshCoreServerState()
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                try
                {
                    IncrementLoading();
                    this.LastLoadError = null;
                    var result = this.modelBase.RunspacePool.ExecuteCommand(
                        new GetXAServerByName() { ComputerName = this.modelBase.ConnectionTarget, Full = true });
                    UpdateCoreStateForServers(result);
                }
                catch (InvalidRunspacePoolStateException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    this.LastLoadError = ex.Message;
                }
                catch (CmdletInvocationException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    this.LastLoadError = ex.Message;
                }
                catch (Exception ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    throw;
                }
                finally
                {
                    DecrementLoading();
                }
            });
        }

        /// <summary>
        /// Does a load query to refresh all the server load state.
        /// </summary>
        private void RefreshServerLoad()
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                try
                {
                    IncrementLoading();
                    var result = this.modelBase.RunspacePool.ExecuteCommand(
                        new GetXAServerLoadByName() { ComputerName = this.modelBase.ConnectionTarget });
                    UpdateLoadForServers(result);
                }
                catch (InvalidRunspacePoolStateException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                }
                catch (CmdletInvocationException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                }
                catch (Exception ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    throw;
                }
                finally
                {
                    DecrementLoading();
                }
            });
        }

        /// <summary>
        /// Updates the core server state. Server objects are added/removed as required while
        /// existing objects will get updated.
        /// </summary>
        /// <param name="source">The source set of server state.</param>
        private void UpdateCoreStateForServers(IEnumerable<XAServer> source)
        {
            if (null == source)
            {
                return;
            }

            List<IServerModel> addedServers = new List<IServerModel>();
            List<IServerModel> removedServers = new List<IServerModel>();
            List<Tuple<ServerModel, XAServer>> updateServers = new List<Tuple<ServerModel, XAServer>>();

            lock (this.stateLock)
            {
                // Add/update servers, as well as building the set of removed servers.
                HashSet<string> buildRemoved = new HashSet<string>(this.servers.Keys);
                foreach (var s in source)
                {
                    buildRemoved.Remove(s.ServerName);

                    if (this.servers.ContainsKey(s.ServerName))
                    {
                        var server = this.servers[s.ServerName];
                        updateServers.Add(new Tuple<ServerModel, XAServer>(server, s));
                    }
                    else
                    {
                        var server = new ServerModel(s);
                        this.servers[s.ServerName] = server;
                        addedServers.Add(server);
                    }
                }

                // Remove all the servers that were not in our source.
                foreach (var s in buildRemoved)
                {
                    var server = this.servers[s];
                    removedServers.Add(server);
                    this.servers.Remove(s);
                }
            }

            // Trigger the updating of the server state outside our internal lock.
            foreach (var update in updateServers)
            {
                update.Item1.Update(update.Item2);
            }

            // Fire change event.
            if (addedServers.Count > 0 || removedServers.Count > 0)
            {
                ServersChangedArgs args = new ServersChangedArgs(addedServers, removedServers);
                this.ServersChanged.Raise(this, args);
            }
        }

        /// <summary>
        /// Updates all the load state for all the servers. This update will also create new
        /// server objects if required, but it will not delete them. Only the core update
        /// function will delete server objects.
        /// </summary>
        /// <param name="source">The source set of load info.</param>
        private void UpdateLoadForServers(IEnumerable<XAServerLoad> source)
        {
            if (null == source)
            {
                return;
            }

            List<IServerModel> addedServers = new List<IServerModel>();
            List<Tuple<ServerModel, XAServerLoad>> updateServers = new List<Tuple<ServerModel, XAServerLoad>>();

            lock (this.stateLock)
            {
                foreach (var s in source)
                {
                    if (this.servers.ContainsKey(s.ServerName))
                    {
                        updateServers.Add(new Tuple<ServerModel, XAServerLoad>(this.servers[s.ServerName], s));
                    }
                    else
                    {
                        var server = new ServerModel(s);
                        this.servers[s.ServerName] = server;
                        addedServers.Add(server);
                    }
                }
            }

            foreach (var update in updateServers)
            {
                update.Item1.UpdateLoad(update.Item2);
            }

            // Fire change event.
            if (addedServers.Count > 0)
            {
                ServersChangedArgs args = new ServersChangedArgs(addedServers, null);
                this.ServersChanged.Raise(this, args);
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Fired whenever a public property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Fires the property change event. Maybe called from any thread - we always fire the
        /// event on the main UI thread.
        /// </summary>
        /// <param name="propertyName">The name of the property that changed.</param>
        private void OnPropertyChanged(string propertyName)
        {
            NotifyHelper.VerifyProperty(propertyName, this);
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion // INotifyPropertyChanged Members
    }
}
